package de.justphil.tcg.tcgserver.websockets;

import java.util.List;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;
import java.util.concurrent.locks.Lock;

import net.jcip.annotations.ThreadSafe;

import org.glassfish.grizzly.http.HttpRequestPacket;
import org.glassfish.grizzly.websockets.DataFrame;
import org.glassfish.grizzly.websockets.ProtocolHandler;
import org.glassfish.grizzly.websockets.WebSocket;
import org.glassfish.grizzly.websockets.WebSocketApplication;
import org.glassfish.grizzly.websockets.WebSocketListener;
import org.slf4j.Logger;

import com.hazelcast.config.MapConfig;
import com.hazelcast.core.Hazelcast;
import com.hazelcast.core.IMap;

import static de.justphil.tcg.tcgserver.commons.logging.TCGLogging.*;
import de.justphil.tcg.tcgserver.commons.config.Config;
import de.justphil.tcg.tcgserver.commons.config.GridConfig;
import de.justphil.tcg.tcgserver.commons.domain.Card;
import de.justphil.tcg.tcgserver.commons.domain.collections.CardInstanceCollection;
import de.justphil.tcg.tcgserver.commons.domain.containers.GameExpired;
import de.justphil.tcg.tcgserver.commons.domain.containers.GameOver;
import de.justphil.tcg.tcgserver.commons.exceptions.TCGException;
import de.justphil.tcg.tcgserver.game.GameManager;
import de.justphil.tcg.tcgserver.game.interfaces.Progressing;
import de.justphil.tcg.tcgserver.commons.grid.Grid;
import de.justphil.tcg.tcgserver.commons.util.AtomicIdGenerator;
import de.justphil.tcg.tcgserver.commons.util.MemoryUtil;
import de.justphil.tcg.tcgserver.websockets.interfaces.TCGAppManageable;
import de.justphil.tcg.tcgserver.websockets.interfaces.TCGameCreator;
import de.justphil.tcg.tcgserver.websockets.messages.MessageSubject;
import de.justphil.tcg.tcgserver.websockets.messages.TCGMessage;
import de.justphil.tcg.tcgserver.websockets.services.ConnectionCloseHandler;
import de.justphil.tcg.tcgserver.websockets.services.ConnectionCloseHandler.OnReconnectTimeOutListener;
import de.justphil.tcg.tcgserver.websockets.services.GameService;
import de.justphil.tcg.tcgserver.websockets.services.NewGameMonitor;
import de.justphil.tcg.tcgserver.websockets.services.NewGameMonitor.OnGameExpiredListener;
import de.justphil.tcg.tcgserver.websockets.util.MessageFactory;
import de.justphil.tcg.tcgserver.websockets.util.MessageParser;
import de.justphil.tcg.tcgserver.websockets.util.MessageValidator;
import de.justphil.tcg.tcgserver.websockets.util.MessageWriter;
import de.justphil.tcg.tcgserver.commons.i18n.Error;

/**
 * This class is explicitly designed to be thread-safe. It must (!) be thread-safe because multiple threads
 * out of the Grizzly framework's thread pool are accessing (read & write access) it's instances concurrently.
 * Furthermore, it must be stated that only one instance of this class exists at runtime.
 * 
 * @author pt
 *
 */
@ThreadSafe
public class TCGApplication extends WebSocketApplication implements TCGAppManageable, TCGameCreator, OnReconnectTimeOutListener, OnGameExpiredListener {
	private static final Logger log = logger(TCGApplication.class);
	private static final Logger status = statusLogger(TCGApplication.class);
	
	/*
	 * These fields are resources which are potentially shared among multiple threads,
	 * therefore they must (!) ensure proper synchronization / thread-safety.
	 */
	private final IMap<Long, GameManager> currentGamesByIdMap					= Hazelcast.getMap(GridConfig.GRID_CURRENT_GAMES_BY_ID_MAP);
	private final ConcurrentMap<WebSocket, Long> currentGameIdsByWebSocketMap	= new ConcurrentHashMap<WebSocket, Long>();
	private final ConcurrentMap<Long, WebSocket> currentWebSocketsByPlayerIdMap	= new ConcurrentHashMap<Long, WebSocket>();
	//private final ConnectionMaintainer cm = new ConnectionMaintainer(WebSocketConfig.SCAN_INTERVAL, WebSocketConfig.SCAN_INTERVAL_UNIT);
	private final ConnectionCloseHandler cch = new ConnectionCloseHandler();
	private final NewGameMonitor ngm = new NewGameMonitor();
	
	private GameService gameService;
	
	public TCGApplication() {
		//cm.setConnectionListener(this);
		//cm.start();
		
		cch.setOnReconnectTimeOutListener(this);
		ngm.setOnGameExpiredListener(this);
	}
	
	public void setGameService(GameService gameService) {
		this.gameService = gameService;
	}
	
	/* ############################################################################################ */
	/* ############################################################################################ */
	/* ############################################################################################ */
	// connection maintenance
	
	@Override
	public void onConnect(WebSocket socket) {
		log.info("onConnect()");
	}
	
	/*
	@Override
	public void onConnectionTimeOut(WebSocket socket) {
		log.info("onConnectionTimeOut()");
		
		( (TCGWebSocket) socket ).setConnectionProblem(true);
		notifyAboutConnectionProblem(socket);
	}
	
	@Override
	public void onConnectionRehab(WebSocket socket) {
		log.info("onConnectionRehab()");
		
		( (TCGWebSocket) socket ).setConnectionProblem(false);
		notifyAboutConnectionRehab(socket);
	}
	*/

	@Override
	public void onClose(WebSocket socket, DataFrame frame) {
		log.info("onClose()");
		notifyAboutConnectionClose(socket);
	}
	
	@Override
	public void onReconnectTimeOut(long gameId, long playerId) {
		log.info("onReconnectTimeOut()");
		notifyAboutReconnectTimeOut(gameId, playerId);
	}
	
	@Override
	public void onGameExpired(long gameId) {
		log.info("onGameExpired() -> '{}'", gameId);
		notifyAboutExpiredGame(gameId);
	}

	/* ############################################################################################ */
	/* ############################################################################################ */
	/* ############################################################################################ */
	// Callback methods which are invoked by the Grizzly framework
	
	@Override
	public WebSocket createSocket(ProtocolHandler handler,
			HttpRequestPacket requestPacket, WebSocketListener... listeners) {
		
		return new TCGWebSocket(handler, requestPacket, listeners);
	}
	
	@Override
	public boolean isApplicationRequest(HttpRequestPacket req) {
		return "/game".equals(req.getRequestURI());
	}

	@Override
	public void onMessage(WebSocket socket, String data) {
		log.debug("onMessage() => {}", data);
		
		Lock lock	= null;
		long gameId	= 0;
		
		try {
			// Transform string -> object
			TCGMessage msg = MessageParser.parse(data);
			
			// Is this WebSocket already mapped to a game?
			gameId = getCurrentGameIdByWebSocket(socket);
			
			// Yes
			if (gameId > 0) {
				// Only forward 'game-p' and 'game-q' messages
				if ( !MessageSubject.isProgressSubject(msg) && !MessageSubject.isQuitSubject(msg) ) {
					log.info(Error.CLIENT_ERROR_INVALID_WEBSOCKET_MESSAGE_SUBJECT);
					throw new TCGException(Error.CLIENT_ERROR_INVALID_WEBSOCKET_MESSAGE_SUBJECT);
				}
			}
			// No
			else {
				// Only forward 'game-j' messages
				if ( !MessageSubject.isJoinSubject(msg) ) {
					log.info(Error.CLIENT_ERROR_INVALID_WEBSOCKET_MESSAGE_SUBJECT);
					throw new TCGException(Error.CLIENT_ERROR_INVALID_WEBSOCKET_MESSAGE_SUBJECT);
				}
				
				// Only forward 'game-j' messages containing a gameId, playerId and OTP
				if ( !MessageValidator.isValidJoinMessage(msg) ) {
					log.info(Error.CLIENT_ERROR_GAME_ID_PLAYER_ID_OR_OTP_MISSING);
					throw new TCGException(Error.CLIENT_ERROR_GAME_ID_PLAYER_ID_OR_OTP_MISSING);
				}
				
				gameId = msg.getJoinFragment().getGameId();
				
				// Game available?
				if (!isCurrentGame(gameId)) {
					log.info(Error.CLIENT_ERROR_GAME_NOT_AVAILABLE);
					throw new TCGException(Error.CLIENT_ERROR_GAME_NOT_AVAILABLE);
				}
			}
			
			// It's all good -> forward message to the corresponding Game object
			lock = Grid.getCurrentGameLock(gameId);
			lock.lock(); 	// obtain the lock of the gameId object in order to
							// synchronize access to the game object which will
							// be 'copied' from the cluster
			log.info("Locked game '{}'", gameId);
			Progressing game = getCurrentGame(gameId);	// copy game object from cluster into
														// local thread stack
			
			if (game != null) {							// It can happen that the 'NewGameMonitor' thread has
				game.progress(socket, msg, data);		// meanwhile (during the execution of this method) cleaned up the game.
			}											// Consequently, this 'not null' test is mandatory!
			else {
				log.info("Game expiration ({}) during onMessage()!", gameId);
			}
		}
		catch (TCGException e) {
			//log.warn("TCGException during onMessage()!");
			
			//e.printStackTrace();
			
			socket.send(MessageWriter.writeJson(MessageFactory.errorMessage(e.getMessage())));
			socket.close();
		}
		finally {
			if (lock != null) {
				lock.unlock();
				
				if (gameId > 0)
					log.info("Unlocked game '{}'", gameId);
			}
		}
	}
	
	/* ############################################################################################ */
	/* ############################################################################################ */
	/* ############################################################################################ */
	// Private methods
	
	private Progressing getCurrentGame(long gameId) {
		return currentGamesByIdMap.get(gameId);
	}
	
	private boolean isCurrentGame(long gameId) {
		return currentGamesByIdMap.containsKey(gameId);
	}
	
	private long getCurrentGameIdByWebSocket(WebSocket socket) {
		Long out = currentGameIdsByWebSocketMap.get(socket);
		if (out == null) {
			return 0L;
		}
		
		return out.longValue();
	}
	
	/*
	private WebSocketListener[] addConnectionMaintainer(WebSocketListener[] listeners) {
		int listenerCount = listeners.length;
		WebSocketListener[] newListeners = new WebSocketListener[listenerCount + 1];
		newListeners[0] = cm;
		
		for(int i = 1; i < newListeners.length; i++) {
			newListeners[i] = listeners[i-1];
		}
		
		return newListeners;
	}
	*/
	
	/*
	private void notifyAboutConnectionProblem(WebSocket socket) {
		long gameId = getCurrentGameIdByWebSocket(socket);
		
		assert gameId > 0 : "gameId <= 0";
		
		Lock lock = null;
		try {
			// Notify the corresponding Game that this player has a connection problem
			lock = Grid.getLock(gameId);
			lock.lock(); 	// obtain the lock of the gameId object in order to
							// synchronize access to the game object which will
							// be 'copied' from the cluster
			log.info("Locked game '{}'", gameId);
			Progressing game = getCurrentGame(gameId);	// copy game object from cluster into
														// local thread stack
			
			if (game != null) {							// It can happen that the 'NewGameMonitor' thread has
				game.onConnectionProblem(socket);		// meanwhile (during the execution of this method) cleaned up the game.
			}											// Consequently, this 'not null' test is mandatory!
			else {
				log.info("Game expiration ({}) during notifyAboutConnectionProblem()!", gameId);
			}
		}
		finally {
			if (lock != null) {
				lock.unlock();
				log.info("Unlocked game '{}'", gameId);
			}
		}
	}
	
	private void notifyAboutConnectionRehab(WebSocket socket) {
		long gameId = getCurrentGameIdByWebSocket(socket);
		
		assert gameId > 0 : "gameId <= 0";
		
		Lock lock = null;
		try {
			// Notify the corresponding Game that this player's connection problem has been resolved
			lock = Grid.getLock(gameId);
			lock.lock(); 	// obtain the lock of the gameId object in order to
							// synchronize access to the game object which will
							// be 'copied' from the cluster
			log.info("Locked game '{}'", gameId);
			Progressing game = getCurrentGame(gameId);	// copy game object from cluster into
														// local thread stack
			
			if (game != null) {						// It can happen that the 'NewGameMonitor' thread has
				game.onConnectionRehab(socket);		// meanwhile (during the execution of this method) cleaned up the game.
			}										// Consequently, this 'not null' test is mandatory!
			else {
				log.info("Game expiration ({}) during notifyAboutConnectionRehab()!", gameId);
			}
		}
		finally {
			if (lock != null) {
				lock.unlock();
				log.info("Unlocked game '{}'", gameId);
			}
		}
	}
	*/
	
	private void notifyAboutConnectionClose(WebSocket socket) {
		long gameId = getCurrentGameIdByWebSocket(socket);
		if (gameId > 0) {
			log.info("WebSocket is an in-game WebSocket and associated with a player");
			
			// LOOKUP BY VALUE IS VERY SLOW!!! BUT ASSERTIONS WILL BE DISABLED IN PRODUCTION ANYWAY !!!
			assert currentWebSocketsByPlayerIdMap.containsValue(socket) :
				"currentWebSocketsByPlayerIdMap does NOT contain WebSocket although currentGameIdsByWebSocketMap does.";
			
			assert isCurrentGame(gameId) : "gameId '"+gameId+"' is not part of currentGamesByIdMap.";
			
			Lock lock = null;
			long playerId = 0;
			try {
				/* Get the playerId of the player that was connected through this WebSocket */
				lock = Grid.getCurrentGameLock(gameId);
				lock.lock(); 	// obtain the lock of the gameId object in order to
								// synchronize access to the game object which will
								// be 'copied' from the cluster
				log.debug("Locked game '{}'", gameId);
				GameManager game = (GameManager) getCurrentGame(gameId);	// copy game object from cluster into
																			// local thread stack
				
				if (game != null) {
					playerId = game.getPlayerByWebSocket(socket).getPlayerId();
					
					assert playerId > 0 : "playerId <= 0 although it shouldn't be.";
					
					log.debug("Retrieved playerId associated with this WebSocket.");
					
					// tell the ConnectionCloseHandler to start a countdown
					cch.onClose(gameId, playerId);
					
					// forward the disconnect event to the game
					game.onClose(socket);
				}
				else {
					log.info("Game expiration ({}) during notifyAboutConnectionClose()!", gameId);
				}
			}
			finally {
				if (lock != null) {
					lock.unlock();
					log.info("Unlocked game '{}'", gameId);
				}
			}
		}
	}
	
	private void notifyAboutReconnectTimeOut(long gameId, long playerId) {
		Lock lock = null;
		try {
			
			/*
			 *  
			 * Notify the corresponding Game that the timeout for the disconnected player has expired.
			 * (Player hasn't reconnected within the specified time.)
			 * 
			 */
			
			lock = Grid.getCurrentGameLock(gameId);
			lock.lock(); 	// obtain the lock of the gameId object in order to
							// synchronize access to the game object which will
							// be 'copied' from the cluster
			log.info("Locked game '{}'", gameId);
			Progressing game = getCurrentGame(gameId);	// copy game object from cluster into
														// local thread stack
			
			if (game != null) {
				game.onReconnectTimeOut(playerId);		// Hence, this invocation is now thread-safe
			}
			else {
				log.info("Game expiration ({}) during notifyAboutReconnectTimeOut()!", gameId);
			}
		}
		finally {
			if (lock != null) {
				lock.unlock();
				log.info("Unlocked game '{}'", gameId);
			}
		}
	}
	
	private void notifyAboutExpiredGame(long gameId) {
		Lock lock = null;
		try {
			/* Notify the corresponding Game that it has expired (players haven't connected within the specified time) */
			lock = Grid.getCurrentGameLock(gameId);
			lock.lock(); 	// obtain the lock of the gameId object in order to
							// synchronize access to the game object which will
							// be 'copied' from the cluster
			log.info("Locked game '{}'", gameId);
			Progressing game = getCurrentGame(gameId);	// copy game object from cluster into
														// local thread stack
			game.onGameExpired();						// Hence, this invocation is now thread-safe
		}
		finally {
			if (lock != null) {
				lock.unlock();
				log.info("Unlocked game '{}'", gameId);
			}
		}
	}
	
	/* ############################################################################################ */
	/* ############################################################################################ */
	/* ############################################################################################ */
	// These methods establish the public interface of this class to the outside world
	
	public void createNewGame(long gameId, List<Long> playerIds, CardInstanceCollection[] decks, String[] otps,
			Map<Long, Integer> playerToDeckMapping, Map<Long, Integer> playerToOtpMapping, Map<Long, Card> cardMap, String otp) {
		
		assert gameId > 0 : "gameId <= 0 is not allowed!";
		
		
		GameManager game = new GameManager(this, gameId, playerIds,
				decks, otps, playerToDeckMapping, playerToOtpMapping, cardMap, otp);
		currentGamesByIdMap.put(gameId, game);
		ngm.monitorNewGame(gameId);
		
		printStatus();
	}
	
	// This method exists only for testing purposes.
	public long createNewGameLocally(List<Long> playerIds, CardInstanceCollection[] decks, String[] otps,
					Map<Long, Integer> playerToDeckMapping, Map<Long, Integer> playerToOtpMapping, Map<Long, Card> cardMap, String otp) {
		
		long gameId = AtomicIdGenerator.nextGameId() + 10;
		GameManager game = new GameManager(this, gameId, playerIds, decks, otps, playerToDeckMapping, playerToOtpMapping, cardMap, otp);
		currentGamesByIdMap.put(gameId, game);
		ngm.monitorNewGame(gameId);

		return gameId;
	}

	@Override
	public void removeGame(long gameId) {
		currentGamesByIdMap.remove(gameId);
	}
	
	@Override
	public void commitGameChange(long gameId, GameManager game) {
		currentGamesByIdMap.replace(gameId, game);
	}

	@Override
	public void createWebSocketToGameIdMapping(WebSocket socket, long gameId) {
		currentGameIdsByWebSocketMap.put(socket, gameId);
	}

	@Override
	public void removeWebSocketToGameIdMapping(WebSocket socket) {
		currentGameIdsByWebSocketMap.remove(socket);
	}
	
	@Override
	public TCGWebSocket getWebSocketByPlayerId(long playerId) {
		return (TCGWebSocket) currentWebSocketsByPlayerIdMap.get(playerId);
	}
	
	@Override
	public void createPlayerIdToWebSocketMapping(long playerId, WebSocket socket) {
		currentWebSocketsByPlayerIdMap.put(playerId, socket);
	}
	
	@Override
	public void removePlayerIdToWebSocketMapping(long playerId) {
		currentWebSocketsByPlayerIdMap.remove(playerId);
	}
	
	/*
	@Override
	public void maintainInGameWebSocket(WebSocket socket) {
		cm.addInGameWebSocket(socket);
	}
	*/
	
	@Override
	public void signalReconnectedPlayer(long playerId) {
		cch.onReconnect(playerId);
	}
	
	@Override
	public void signalNewGameStarted(long gameId) {
		ngm.onNewGameStarted(gameId);
	}
	
	@Override
	public void signalFirstPlayerJoined(long gameId) {
		ngm.onFirstPlayerJoined(gameId);
	}
	
	@Override
	public void signalGameExpired(long gameId, String gameOtp) {
		gameService.sendObject( new GameExpired( gameId, gameOtp ) );
	}

	@Override
	public void signalGameOver(long gameId, long winnerId, String gameOtp) {
		gameService.sendObject( new GameOver(gameId, winnerId, gameOtp) );
	}
	
	@Override
	public void cleanUp(long gameId, long[] playerIds, boolean cleanUpNewGameMonitor) {
		log.debug("cleanUp()...............");
		
		TCGWebSocket socket;
		for (long playerId : playerIds) {
			cch.cleanUp(playerId);
			
			socket = getWebSocketByPlayerId(playerId);
			if (socket != null) {
				log.debug("Removing WebSocket: {}", socket.toString());
				removeWebSocketToGameIdMapping(socket);
				
				if (socket.isConnected()) {
					log.debug("Closing WebSocket: {}", socket.toString());
					socket.close();
				}
			}
			
			removePlayerIdToWebSocketMapping(playerId);
		}
		
		if (cleanUpNewGameMonitor) {
			ngm.cleanUp(gameId);
		}
		
		log.debug("Removing Current Game: {}", gameId);
		removeGame(gameId);
		
		Runtime.getRuntime().gc();
		
		printStatus();
		log.debug("cleanUp()...............DONE!");
	}
	
	/* ############################################################################################ */
	/* ############################################################################################ */
	/* ############################################################################################ */
	
	@Override
	public void printStatus() {
		if (Config.DEBUG) {
			String sep = "### ##################################### ###";
			
			// header
			status.info(sep);
			status.info(sep);
			status.info("### INFORMATION ABOUT THE TCG APPLICATION OBJECT ###");
			status.info(sep);
			status.info(sep);
			
			com.hazelcast.config.Config cfg = Hazelcast.getConfig();
			Map<String, MapConfig> mapConfigs = cfg.getMapConfigs();
			for (MapConfig mapConfig : mapConfigs.values()) {
				status.info("### Map '{}' ### cacheValues: {}", mapConfig.getName(), mapConfig.isCacheValue());
			}
			
			// currentGamesByIdMap
			status.info(sep);
			status.info("### currentGamesByIdMap (Running Games) ###");
			status.info(sep);
			for (long gameId : currentGamesByIdMap.keySet()) {
				status.info("### {} => {} ###", gameId, currentGamesByIdMap.get(gameId).getStateAsString());
			}
			
			// currentGameIdsByWebSocketMap
			status.info(sep);
			status.info("### currentGameIdsByWebSocketMap (Connected WebSockets) ###");
			status.info(sep);
			for (WebSocket socket : currentGameIdsByWebSocketMap.keySet()) {
				status.info("### {} => {} ###", socket.toString(), currentGameIdsByWebSocketMap.get(socket).longValue());
			}
			
			// currentWebSocketsByPlayerIdMap
			status.info(sep);
			status.info("### currentWebSocketsByPlayerIdMap (playerId => WebSocket) ###");
			status.info(sep);
			for (long playerId : currentWebSocketsByPlayerIdMap.keySet()) {
				status.info("### {} => {} ###", playerId, currentWebSocketsByPlayerIdMap.get(playerId).toString());
			}
			
			// All WebSockets that are known by Grizzly
			status.info(sep);
			status.info("### WebSocketApplication.getWebSockets() ###");
			status.info(sep);
			for (WebSocket socket : getWebSockets()) {
				status.info("### {} ###", socket.toString());
			}
			
			// Memory information
			Runtime runtime = Runtime.getRuntime();
			long total = runtime.totalMemory();
			long free = runtime.freeMemory();
			long used = total - free;
			
			String t = MemoryUtil.formatBytes(total);
			String f = MemoryUtil.formatBytes(free);
			String u = MemoryUtil.formatBytes(used);
			
			status.info(sep);
			status.info("### Memory Information ###");
			status.info(sep);
			status.info("### Total Memory: {} MB ### Used Memory: {} MB (Free: {} MB)",
					new Object[] { t, u, f }
			);
		}
	}	
	
}
